//
//  ECSJavaScriptContext.m
//  ECS DevelopKit
//
//  Created by LittoCats on 8/27/14.
//  Copyright (c) 2014 Littocats. All rights reserved.
//

#import "ECSJavaScriptContext.h"
#import "ECSJavaScriptFunction.h"
#import "ECSBlockInvocation.h"

#import <objc/runtime.h>
#import <objc/message.h>
#import <JavaScriptCore/JavaScriptCore.h>

@interface _ECSJavaScriptFunction : ECSJavaScriptFunction

@property (nonatomic, strong) NSString *function;

@property (nonatomic, strong) NSString *context;

+ (instancetype)function:(JSObjectRef)jsFunction withContext:(ECSJavaScriptContext *)context;
@end

@interface ECSJavaScriptContext ()

@property (nonatomic, assign) JSGlobalContextRef context;

@property (nonatomic, strong) NSMutableSet *PropertyContainer;

@property (nonatomic, strong) NSString *name;

@end

@implementation ECSJavaScriptContext

- (id)init
{
    self = [super init];
    if (self) {
        self.context = JSGlobalContextCreate(NULL);
        self.PropertyContainer = [[NSMutableSet alloc] initWithCapacity:64];
        [self addProperty:self.name  withName:@"name"];
    }
    return self;
}

- (void)dealloc
{
    JSGlobalContextRelease(_context);
}

- (NSString *)name
{
    if (!_name) {
        static int n;
        if (!n) n = 1;
        self.name = [[NSString alloc] initWithFormat:@"ECSJavaScriptContext_%i_%p",n,self];
        [[self.class contextTable] setObject:self forKey:_name];
    }
    return _name;
}
/**
 *  向context中添加属性
 *  @propertyName 属性名
 *  @property 属性值
 */
- (void)addProperty:(id)property withName:(NSString *)name
{
    JSStringRef jsName = JSStringCreateWithCFString((__bridge CFStringRef)(name));
    JSObjectRef jsProperty = NULL;
    
    if ([property isKindOfClass:NSClassFromString(@"NSBlock")]) {
        jsProperty = JSObjectMakeFunctionWithCallback(_context, jsName, ECSJavaScriptCallBackChannel);
        [self.PropertyContainer addObject:property];
        [[self.class ECSJavaScriptContextPropertyWrapper] setObject:property forKey:[[NSString alloc] initWithFormat:@"ECSJavaScriptCallBackBlock_%p",jsProperty]];
    }else{
        jsProperty = JSValueToObject(_context, [self JSValueFormNSObject:property], NULL) ;
    }
    
    JSValueRef exception = NULL;
    JSObjectSetProperty(_context, JSContextGetGlobalObject(_context), jsName, jsProperty, kJSPropertyAttributeNone, &exception);
    [self handleException:exception];
    
    JSStringRelease(jsName);
}

JSValueRef ECSJavaScriptCallBackChannel(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception)
{
    id nsblock = [[ECSJavaScriptContext ECSJavaScriptContextPropertyWrapper] objectForKey:[[NSString alloc] initWithFormat:@"ECSJavaScriptCallBackBlock_%p",function]];
    // 解析 ECSJavaScriptContext
    JSStringRef jsName = JSStringCreateWithUTF8CString("name");
    JSValueRef nameValue = JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), jsName, NULL);
    JSStringRelease(jsName);
    jsName = JSValueToStringCopy( ctx, nameValue, NULL );
    NSString *name = (__bridge_transfer NSString *)JSStringCopyCFString( kCFAllocatorDefault, jsName );
    if (!name)
        @throw [[NSException alloc] initWithName:@"ECSJavaScriptCallBackChannel" reason:@"ECSJavaScriptContext's name is not found in context ." userInfo:nil];
    JSStringRelease(jsName);
    
    ECSJavaScriptContext *context = [[ECSJavaScriptContext contextTable] objectForKey:name];
    NSMutableArray *argArray = [NSMutableArray new];
    for (int index = 0; index < argumentCount; index ++) {
        [argArray addObject:[context NSObjectFromJSValue:arguments[index]]];
    }
    
    id result = [ECSBlockInvocation invokeBlock:nsblock withArguments:argArray];
    return [context JSValueFormNSObject:result];
}
/**
 *  运行 javascript
 */
- (id)evaluateJavaScript:(NSString *)script
{
    JSValueRef exception = NULL;
    JSStringRef jScript = JSStringCreateWithCFString((__bridge CFStringRef)(script));
    JSValueRef result = JSEvaluateScript(_context, jScript, NULL, NULL, 0, &exception);
    JSStringRelease(jScript);
    [self handleException:exception];
    return [self NSObjectFromJSValue:result];
}

/**
 *  调用 js 环境中的function
 */
- (id)callFunction:(NSString *)function withArguments:(id)arg,...
{
    va_list arglist;
    va_start(arglist, arg);
    return [self callFunction:function withArguments:arg va_list:arglist];
}
- (id)callFunction:(NSString *)function withArguments:(id)arg va_list:(va_list)vList
{
    JSValueRef exception = NULL;
    JSObjectRef func = JSValueToObject(_context, self.class.JSProperty(_context, JSContextGetGlobalObject(_context), function), &exception);
    [self handleException:exception];
    
    NSMutableArray *argumentsA = [NSMutableArray arrayWithObjects:arg, nil];
    if (arg) {
        id argn = va_arg(vList, id);
        while (argn) {
            [argumentsA addObject:argn];
            argn = va_arg(vList, id);
        }
    }
    va_end(vList);
    void *buffer = malloc(sizeof(JSValueRef) * argumentsA.count);
    
    for (int index = 0; index < argumentsA.count; index ++) {
        JSValueRef value = [self JSValueFormNSObject:argumentsA[index]];
        memcpy(buffer + sizeof(JSValueRef)*index, &value, sizeof(JSValueRef));
    }
    
    JSValueRef result = NULL;
    if (JSObjectIsFunction(_context, func))
        result = JSObjectCallAsFunction(_context, func, NULL, argumentsA.count, buffer, &exception);
    free(buffer);
    
    [self handleException:exception];
    return [self NSObjectFromJSValue:result];
}
/***************************************************************************************************/

+ (NSMapTable *)ECSJavaScriptContextPropertyWrapper
{
    static NSMapTable *wrapper = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        wrapper = [NSMapTable strongToWeakObjectsMapTable];
    });
    return wrapper;
}

/***************************************************************************************************/
- (void)handleException:(JSValueRef)exception
{
    if (exception){
        @throw [[NSException alloc] initWithName:@"ECSJavaScriptContext" reason:[[NSString alloc] initWithFormat:@"\n%@\n%@",[self NSStringFromJSValue:exception],[self NSObjectFromJSValue:exception]] userInfo:nil];
    }
}

/***************************************************************************************************/

+ (JSValueRef (^)(JSContextRef ctx, JSObjectRef jsObj, NSString *name))JSProperty
{
    return ^JSValueRef (JSContextRef ctx, JSObjectRef jsObj, NSString *name){
        JSStringRef pName = JSStringCreateWithCFString((__bridge CFStringRef)(name));
        JSValueRef property = JSObjectGetProperty(ctx, jsObj, pName, NULL);
        JSStringRelease(pName);
        
        return property;
    };
}

- (NSString *)NSStringFromJSValue:(JSValueRef)value
{
    JSStringRef jsString = JSValueToStringCopy( _context, value, NULL );
    if( !jsString ) return nil;
    
    NSString *string = (__bridge_transfer NSString *)JSStringCopyCFString( kCFAllocatorDefault, jsString );
    JSStringRelease( jsString );
    
    return string;
}
- (JSValueRef)JSValueFromNSString:(NSString *)string
{
    JSStringRef jstr = JSStringCreateWithCFString((__bridge CFStringRef)string);
    JSValueRef ret = JSValueMakeString(_context, jstr);
    JSStringRelease(jstr);
    return ret;
}

// JSValueToNSNumber blindly assumes that the given JSValueRef is a
// a number. Everything else will be silently converted to 0.
// This functions comes in a 64bit and 32bit flavor, since the NaN-Boxing
// in JSC works a bit differently on each platforms. For an explanation
// of the taggging refer to JSC/runtime/JSCJSValue.h

- (NSNumber *)NSNumberFromJSValue:(JSValueRef)value
{
//#if __LP64__ // arm64 version
//    union {
//        int64_t asInt64;
//        double asDouble;
//        struct { int32_t asInt; int32_t tag; } asBits;
//    } taggedValue = { .asInt64 = (int64_t)value };
//    
//#define DoubleEncodeOffset 0x1000000000000ll
//#define TagTypeNumber 0xffff0000
//#define ValueTrue 0x7
//    
//    if( (taggedValue.asBits.tag & TagTypeNumber) == TagTypeNumber ) {
//        return @(taggedValue.asBits.asInt);
//    }
//    else if( taggedValue.asBits.tag & TagTypeNumber ) {
//        taggedValue.asInt64 -= DoubleEncodeOffset;
//        return @(taggedValue.asDouble);
//    }
//    else if( taggedValue.asBits.asInt == ValueTrue ) {
//        return @1.0;
//    }
//    else {
//        return @0; // false, undefined, null, object
//    }
//#else // armv7 armv7s version
//    struct {
//        unsigned char cppClassData[4];
//        union {
//            double asDouble;
//            struct { int32_t asInt; int32_t tag; } asBits;
//        } payload;
//    } *decoded = (void *)value;
//    
//    return @(decoded->payload.asBits.tag < 0xfffffff9
//    ? decoded->payload.asDouble
//    : decoded->payload.asBits.asInt);
//#endif
    return @(JSValueToNumber(_context, value, NULL));
}

- (JSValueRef)JSValueFormNSObject:(id)obj
{
    JSValueRef ret = NULL;
    
    // String
    if( [obj isKindOfClass:NSString.class])
        ret = [self JSValueFromNSString:obj];
    
    // Number or Bool
    else if( [obj isKindOfClass:NSNumber.class] ) {
        NSNumber *number = (NSNumber *)obj;
        if( strcmp(number.objCType, @encode(BOOL)) == 0 ) {
            ret = JSValueMakeBoolean(_context, number.boolValue);
        }
        else {
            ret = JSValueMakeNumber(_context, number.doubleValue);
        }
    }
    
    // Array
    else if( [obj isKindOfClass:NSArray.class] ) {
        NSArray *array = (NSArray *)obj;
        JSValueRef *args = malloc(array.count * sizeof(JSValueRef));
        for( int i = 0; i < array.count; i++ ) {
            args[i] = [self JSValueFormNSObject:array[i]];
        }
        ret = JSObjectMakeArray(_context, array.count, args, NULL);
        free(args);
    }
    
    // Dictionary
    else if( [obj isKindOfClass:NSDictionary.class] ) {
        NSDictionary *dict = (NSDictionary *)obj;
        ret = JSObjectMake(_context, NULL, NULL);
        for( NSString *key in dict ) {
            JSStringRef jsKey = JSStringCreateWithUTF8CString(key.UTF8String);
            JSValueRef value = [self JSValueFormNSObject:dict[key]];
            JSObjectSetProperty(_context, (JSObjectRef)ret, jsKey, value, kJSPropertyAttributeNone, NULL);
            JSStringRelease(jsKey);
        }
    }
    // Date
    else if( [obj isKindOfClass:NSDate.class] ) {
        NSDate *date = (NSDate *)obj;
        JSValueRef timestamp = JSValueMakeNumber(_context, date.timeIntervalSince1970 * 1000.0);
        ret = JSObjectMakeDate(_context, 1, &timestamp, NULL);
    }
    
    //NSValue
    else if ([obj isKindOfClass:NSValue.class]){
        const char *type = [obj objCType];
        NSDictionary *objc;
        if (strcmp(type, @encode(CGSize)) == 0) {
            CGSize size = [obj CGSizeValue];
            objc = @{@"width":@(size.width),@"height":@(size.height)};
        }else if (strcmp(type, @encode(UIEdgeInsets)) == 0){
            UIEdgeInsets edgeInsets = [obj UIEdgeInsetsValue];
            objc = @{@"top":@(edgeInsets.top),@"left":@(edgeInsets.left),@"right":@(edgeInsets.right),@"bottom":@(edgeInsets.bottom)};
        }else if (strcmp(type, @encode(CGPoint)) == 0){
            CGPoint point = [obj CGPointValue];
            objc = @{@"x":@(point.x),@"y":@(point.x)};
        }else if (strcmp(type, @encode(CGRect)) == 0){
            CGRect rect = [obj CGRectValue];
            objc = @{@"origin":@{@"x": @(rect.origin.x),@"y":@(rect.origin.y)},@"size":@{@"width":@(rect.size.width),@"height":@(rect.size.height)}};
        }else if (strcmp(type, @encode(UIOffset)) == 0){
            UIOffset offset = [obj UIOffsetValue];
            objc = @{@"vertical":@(offset.vertical),@"horizontal":@(offset.horizontal)};
        }
        ret = [self JSValueFormNSObject:objc];
    }
    
    //UIColor 返回 hex string
    else if ([obj isKindOfClass:UIColor.class]){
        CGFloat red,green,blue,alpha;
        [obj getRed:&red green:&green blue:&blue alpha:&alpha];
        ret = [self JSValueFromNSString:[[NSString alloc] initWithFormat:@"#%.2X%.2X%.2X%.2X",(int)(red*255),(int)(green*255),(int)(blue*255),(int)(alpha*255)]];
    }
    
    return ret ? ret : JSValueMakeNull(_context);
}

- (id)NSObjectFromJSValue:(JSValueRef)value
{
    JSType type = JSValueGetType(_context, value);
    
    switch( type ) {
        case kJSTypeString: return [self NSStringFromJSValue:value];
        case kJSTypeBoolean: return [NSNumber numberWithBool:JSValueToBoolean(_context, value)];
        case kJSTypeNumber: return [self NSNumberFromJSValue:value];
        case kJSTypeNull: return NSNull.null;
        case kJSTypeUndefined: return nil;
        case kJSTypeObject: break;
    }
    
    if( type == kJSTypeObject ) {
        JSObjectRef jsObj = JSValueToObject(_context, value, NULL);
        
        //if object is an Function
        if (JSObjectIsFunction(_context, jsObj)) {
            return [_ECSJavaScriptFunction function:jsObj withContext:self];
        }else if( JSValueIsInstanceOfConstructor(_context, jsObj, self.class.JSConstructor(_context, "Array"), NULL) ){
            // Array
            JSStringRef lengthName = JSStringCreateWithUTF8CString("length");
            int count = [[self NSNumberFromJSValue:JSObjectGetProperty(_context, jsObj, lengthName, NULL)] intValue];
            JSStringRelease(lengthName);
            
            NSMutableArray *array = [NSMutableArray arrayWithCapacity:count];
            for( int i = 0; i < count; i++ ) {
                NSObject *obj = [self NSObjectFromJSValue:JSObjectGetPropertyAtIndex(_context, jsObj, i, NULL)];
                [array addObject:(obj ? obj : NSNull.null)];
            }
            return array;
        }else if( JSValueIsInstanceOfConstructor(_context, jsObj, self.class.JSConstructor(_context, "Date"), NULL) ){
            JSStringRef getTimeName = JSStringCreateWithUTF8CString("getTime");
            NSTimeInterval timeInteral = [[self NSNumberFromJSValue:JSObjectCallAsFunction(_context, (JSObjectRef)JSObjectGetProperty(_context, jsObj, getTimeName, NULL), jsObj, 0, NULL, NULL)] doubleValue];
            JSStringRelease(getTimeName);
            return [[NSDate alloc] initWithTimeIntervalSince1970:timeInteral/1000];
        }else {
            // Plain Object
            JSPropertyNameArrayRef properties = JSObjectCopyPropertyNames(_context, jsObj);
            size_t count = JSPropertyNameArrayGetCount(properties);
            
            NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:count];
            for( size_t i = 0; i < count; i++ ) {
                JSStringRef jsName = JSPropertyNameArrayGetNameAtIndex(properties, i);
                NSObject *obj = [self NSObjectFromJSValue:JSObjectGetProperty(_context, jsObj, jsName, NULL)];
                
                NSString *name = (__bridge_transfer NSString *)JSStringCopyCFString( kCFAllocatorDefault, jsName );
                dict[name] = obj ? obj : NSNull.null;
            }
            
            JSPropertyNameArrayRelease(properties);
            return dict;
        }
    }
    return nil;
}

/**************************************************************************************************/

+ (JSObjectRef (^)(JSContextRef ctx, char* name))JSConstructor
{
    return ^JSObjectRef (JSContextRef ctx, char* name){
        JSStringRef constructorName = JSStringCreateWithUTF8CString(name);
        JSValueRef exception = NULL;
        JSObjectRef constructor = (JSObjectRef)JSObjectGetProperty(ctx, JSContextGetGlobalObject(ctx), constructorName, &exception);
        JSStringRelease(constructorName);
        return constructor;
    };
}

+ (NSMapTable *)contextTable
{
    static NSMapTable *table = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        table = [NSMapTable strongToWeakObjectsMapTable];
    });
    return table;
}
@end


@implementation _ECSJavaScriptFunction

+ (instancetype)function:(JSObjectRef)jsFunction withContext:(ECSJavaScriptContext *)context
{
    static int n;
    if (!n) n = 1;
    _ECSJavaScriptFunction *function = [self new];
    function.function = [NSString stringWithFormat:@"_ECSJavaScriptFunction_%i_%p",n,jsFunction];
    JSStringRef functionName = JSStringCreateWithCFString((__bridge CFStringRef)(function.function));
    JSObjectSetProperty(context.context, JSContextGetGlobalObject(context.context), functionName, jsFunction, kJSPropertyAttributeReadOnly, NULL);
    JSStringRelease(functionName);

    
    function.context = context.name;
    return function;
}

- (id)applyWithArguments:(id)arg, ...
{
    va_list vList;
    va_start(vList, arg);
    
    return [self applyWithArguments:arg va_list:vList];
}

- (id)applyWithArguments:(id)arg va_list:(va_list)vList
{
    ECSJavaScriptContext *context = [[ECSJavaScriptContext contextTable] objectForKey:_context];
    return [context callFunction:_function withArguments:arg va_list:vList];
}

@end